// vim: ft=glsl noet ts=4 sw=4 sts=0
R"vertex(
#version 330 core
layout(location = 0) in vec2 position;
out vec3 dir;
uniform float focal;
uniform mat3 camera_axis;
const vec2 aspect_correction = vec2(16.0/9.0, 1);

void main()
{
	gl_Position = vec4(position, 0, 1);
	vec2 uv = position * aspect_correction;
	dir = camera_axis * vec3(uv, -focal);
}

)vertex"

, /////////////////////////////////////////////////////////////////////////

R"fragment(
#version 330 core
in vec3 dir;
out vec3 color;

uniform float time;
uniform vec3 camera_pos;
uniform vec3 player_pos;
uniform float player_alpha;
uniform float texture_filtering;

const float small = 5e-3;
const float large = 1e+3;
vec3 ndir = normalize(dir);

vec3 sky_color;
vec3 sun_color;
vec3 sky_diffuse;
float sun_size;
float reflective;
uniform vec3 sun_dir;
#define SKY_COLOR				(vec3(1, 1.1, 1.4)*0.21)
#define SKY_TWILIGHT_COLOR		(vec3(1, 0.6, 0.2)*0.25)
#define SKY_NIGHT_COLOR			(vec3(0.6, 0.5, 0.7)*0.3)
#define SUN_COLOR				(vec3(1.4, 1.2, 1)*0.4)
#define MOON_COLOR				(vec3(1.2, 1.2, 1)*0.9)
#define MOUNTAIN_COLOR			(vec3(0.2, 0.7, 0.1)*0.4)
float night_to_dawn = (clamp(sun_dir.y, -0.3, 0.2) + 0.3) / 0.5;



float lerp(float x, float f, float t) { return (t-f)*x + f; }
float lerp(float x, float xf, float xt, float f, float t) { return (x-xf) / (xt-xf) * (t-f) + f; }

// uniform over [0, 1]
float rand(vec2 co)
{
    return fract(abs(sin(dot(co, vec2(12.9898, 78.233)))) * 43758.5453);
}

float hash()
{
	return rand(vec2(rand(gl_FragCoord.xy), time)) + 1.0 / 512.0;
}




//------ shape
float sphere(vec3 pos, float radius) { return length(pos) - radius; }
float ground(vec3 pos, float y) { return pos.y - y; }
float ubox(vec3 pos, vec3 size) { return length(max(abs(pos) - size, 0)); }




//------ object
struct object
{
	int id;
	float dist;
};
object objects(object a, object b) { return (a.dist < b.dist ? a : b); }
object objects(object a, object b, object c) { return objects(a, objects(b, c)); }
object objects(object a, object b, object c, object d) { return objects(a, objects(b, c, d)); }

// object identifiers
#define OBJECT_SKY				0
#define OBJECT_GROUND			1
#define OBJECT_JADE				2
#define OBJECT_PLASTIC			3
#define OBJECT_PLAYER			4

object obj_sky    (        ) { return object(OBJECT_SKY    , large               ); }
object obj_ground (vec3 pos) { return object(OBJECT_GROUND , ground(pos,     -1 )); }
object obj_jade   (vec3 pos) { return object(OBJECT_JADE   , sphere(pos,      1 )); }
object obj_plastic(vec3 pos) { return object(OBJECT_PLASTIC,   ubox(pos, vec3(1))); }
object obj_player (vec3 pos) { return object(OBJECT_PLAYER , sphere(pos,   0.01 )); }




//------ scene
object scene(vec3 pos)
{
	float oz = pos.z;
	vec3 jade_pos = mod(pos - vec3( 1, 0, 0), vec3(4, 2, 4)) - vec3(4, 2, 4)/2;
	vec3 pltc_pos = mod(pos - vec3(-1, 0, 0), vec3(4, 2.2, 4)) - vec3(4, 2.2, 4)/2;
	jade_pos.z = oz;
	pltc_pos.z = oz;
	object grnd = obj_ground (pos - vec3(0, -1, 0));
	object jade = obj_jade   (jade_pos);
	object pltc = obj_plastic(pltc_pos);

	if (hash() < player_alpha) {
		object plyr = obj_player (pos - player_pos);
		return objects(grnd, jade, pltc, plyr);
	} else {
		return objects(grnd, jade, pltc);
	}
}




//------ render
object hit;
vec3 hit_point;			// valid only when not hit the sky
float dist_to_hit;		// valid only when not hit the sky
float clearness;

#define RENDER_MAX_STEP			500
#define RENDER_RADIUS			80.0
void render()
{
	float dist = 0;
	for (int i=0; i<RENDER_MAX_STEP && dist < RENDER_RADIUS; i++) {
		vec3 p = camera_pos + dist*ndir;
		object nearest = scene(p);
		if (nearest.dist < small) {
			hit = nearest;
			hit_point = p;
			dist_to_hit = dist;
			clearness = (1 - dist/RENDER_RADIUS) * (1 - i/RENDER_MAX_STEP);
			return;
		}
		dist += nearest.dist;
	}
	hit = obj_sky();
	dist_to_hit = RENDER_RADIUS;
}




//------ shading
vec3 diffuse;
float thickness;

#define THICKNESS_MAX_STEP		50
#define MAX_THICKNESS			1
void populate_thickness()
{
	float dist = small*10;
	for (int i=0; i<THICKNESS_MAX_STEP && dist < MAX_THICKNESS; i++) {
		object nearest = scene(hit_point + dist*ndir);
		if (nearest.dist > 0 || -nearest.dist < small) break;
		dist -= nearest.dist;
	}
	thickness = clamp(dist / MAX_THICKNESS, 0, 1);
}

void populate_sky_diffuse()
{
	float a = dot(ndir, sun_dir);
	float suna = (ndir.y < 0 ? 0 : smoothstep(1 - sun_size, 1, a));
	float moona = (ndir.y < 0 ? 0 : -a > 0.998 ? 1 : 0);
	sky_diffuse = mix(sky_color, sun_color, suna);
	sky_diffuse = mix(sky_diffuse, MOON_COLOR, moona);
}

void populate_mountains()
{
	if (ndir.y < 0) return;
	if (dist_to_hit < RENDER_RADIUS * 0.9) return;
	if (ndir.y < pow(cos(ndir.x*sin(ndir.z)*6), 8) * 0.2) {
		hit.id = OBJECT_SKY;
		sky_diffuse = mix(sky_color, MOUNTAIN_COLOR, 0.3*lerp(ndir.y, 0, 0.2, 0, 1));
		return;
	}
	if (ndir.y < lerp(sin(ndir.x*2*cos(ndir.z)), -1, 1, -0.1, 0.3)) {
		hit.id = OBJECT_SKY;
		sky_diffuse = mix(sky_color, MOUNTAIN_COLOR, 0.1*lerp(ndir.y, -0.1, 0.3, 0, 1));
		return;
	}
	if (ndir.y < lerp(sin(ndir.x*2*cos(ndir.y*5)+1), -1, 1, -0.1, 0.3)) {
		hit.id = OBJECT_SKY;
		sky_diffuse = mix(sky_color, MOUNTAIN_COLOR, 0.04*lerp(ndir.y, -0.1, 0.3, 0, 1));
		return;
	}
}

vec3 diffuse_sky() { return sky_diffuse; }

vec3 checkerbox(vec2 uv)
{
	vec2 m = mod(uv, 2) - 1;
	float a = float(m.x * m.y > 0) * 0.5;
	return vec3(a);
}

#define TEXTURE_FILTER_DETAIL		4
#define TEXTURE_FILTER_MAX_NSAMPLE	6
vec3 diffuse_ground()
{
	vec2 uv = hit_point.xz;
	if (texture_filtering == 0) return checkerbox(uv);

	vec2 xduv = dFdx(uv);
	vec2 yduv = dFdy(uv);

	int nsample = clamp(int(TEXTURE_FILTER_MAX_NSAMPLE * texture_filtering), 1, TEXTURE_FILTER_MAX_NSAMPLE);
	int nx = clamp(1 + int(TEXTURE_FILTER_DETAIL * length(xduv)), 1, nsample);
	int ny = clamp(1 + int(TEXTURE_FILTER_DETAIL * length(yduv)), 1, nsample);
	vec2 size = vec2(nx, ny);

	vec3 color = vec3(0);
	for (int y=0; y<ny; y++)
		for (int x=0; x<nx; x++) {
			vec2 p = vec2(x, y) / size;
			color += checkerbox(uv + xduv*p.x + yduv*p.y);
		}
	color /= nx*ny;
	reflective = color[0]*0.7 + 0.2;

	return color;
}

vec3 diffuse_jade()
{
	float boost = mix(1.5, 1, smoothstep(0, 1, thickness));
	float through = mix(0.1, 0.02, smoothstep(0, 1, thickness));
	return mix(vec3(0.2, 0.6, 0.1)/2 * boost, sky_diffuse, through);
}

vec3 diffuse_plastic() { return vec3(0.2, 0.6, 0.1) / 1.2; }

void populate_diffuse()
{
	reflective = 0;
	switch (hit.id) {
		case OBJECT_SKY    : diffuse = diffuse_sky()    ; break;
		case OBJECT_GROUND : diffuse = diffuse_ground() ; break;
		case OBJECT_JADE   : diffuse = diffuse_jade()   ; break;
		case OBJECT_PLASTIC: diffuse = diffuse_plastic(); break;
	}
}


vec3 normal;
void populate_normal()
{
	normal.x = scene(hit_point + vec3(small, 0, 0)).dist - scene(hit_point - vec3(small, 0, 0)).dist;
	normal.y = scene(hit_point + vec3(0, small, 0)).dist - scene(hit_point - vec3(0, small, 0)).dist;
	normal.z = scene(hit_point + vec3(0, 0, small)).dist - scene(hit_point - vec3(0, 0, small)).dist;
	normal = normalize(normal);
}


void populate_player_diffuse()
{
	diffuse = vec3(0.3);
	diffuse *= 1 + dot(ndir, normal);
}



float shadow_darkness;
float shadow_mask;
#define SHADOW_SHARPNESS	16
#define SHADOW_MAX_STEP		60
#define SHADOW_INTENSITY	0.4
#define SHADOW_OFFSET		10*small
void populate_shadow()
{
	shadow_darkness = 1.0;
	float dist = 0;
	vec3 p = hit_point + normal*SHADOW_OFFSET;
	for (int i=0; i<SHADOW_MAX_STEP && dist<RENDER_RADIUS; i++) {
		object nearest = scene(p+dist*sun_dir);
		if (nearest.dist < small) {
			shadow_darkness = 0;
			break;
		}
		shadow_darkness = min(shadow_darkness, SHADOW_SHARPNESS * nearest.dist / dist);
		dist += nearest.dist;
	}
	shadow_mask = smoothstep(0, 1, shadow_darkness);
	shadow_darkness = mix(1, shadow_darkness, SHADOW_INTENSITY);
}


float ao;
#define AO_HARDNESS		4
#define AO_AMOUNT		1
void populate_ao()
{
	float step = 10*small;
	float dist = step;
	ao = 0.0;
	for (int i=0; i<AO_HARDNESS; i++) {
		float nearest = scene(hit_point + dist*normal).dist;
		ao += dist - nearest;
		dist += step;
	}
	ao = clamp(ao, 0, 1);
	ao = mix(1, 1-ao, AO_AMOUNT);
}


void light()
{
	diffuse *= mix(1, clamp(dot(normal, sun_dir), 0, 0.5) + 0.5, night_to_dawn);
	float fresnel = pow(1.0 - max(0, dot(-ndir, normal)), 5);
	reflective = (fresnel - reflective) * fresnel + reflective;
	float spec = pow(max(0, dot(normalize(-ndir + sun_dir), normal)), pow(5, reflective)) * reflective;
	diffuse += shadow_mask * night_to_dawn * sun_color * spec;
	diffuse *= sky_color;
}

void shadow()
{
	diffuse *= mix(1.0, shadow_darkness, night_to_dawn);
}

void ambient_occlusion()
{
	diffuse *= min(1, ao + mix(0.2, 0.0, night_to_dawn));
}


void fog() { diffuse = mix(diffuse, sky_color, smoothstep(0.4, 1, 1 - clearness)); }

void shade()
{
	populate_sky_diffuse();
	populate_mountains();
	if (hit.id == OBJECT_JADE) populate_thickness();
	populate_diffuse();
	if (hit.id == OBJECT_SKY) return;
	populate_normal();
	if (hit.id == OBJECT_PLAYER) {
		populate_player_diffuse();
		return;
	}
	populate_shadow();
	populate_ao();

	light();
	shadow();
	ambient_occlusion();
	fog();
}




//------ animate
void animate()
{
	float brightness;
	if (sun_dir.y > 0) brightness = cos(time*3)*0.05 + lerp(sun_dir.y, 0, 1, 2.5, 3);
	else brightness = mix(1, 2.5, max(sun_dir.y, -0.3) / 0.3 + 1);
	sun_color = SUN_COLOR*brightness;
	sky_color = mix(SKY_TWILIGHT_COLOR, SKY_COLOR, max(sun_dir.y, 0));
	sky_color = mix(SKY_NIGHT_COLOR, sky_color, night_to_dawn);
	sky_color *= brightness;
	sun_size = (cos(time*2) + 10) * small/2;
}





//------ main
void main()
{
	animate();
	render();
	shade();
	color = diffuse;

	// night effect
	color.r = pow(color.r, lerp(night_to_dawn, 3, 1));
	color.g = pow(color.g, lerp(night_to_dawn, 2.5, 1));
}

)fragment"

